﻿//-----------------------------------------------------------------------
// <copyright file="AkkaAssertEqualityComparer.cs" company="Akka.NET Project">
//     Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
//     Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;

namespace Akka.TestKit.Xunit.Internals;

internal static class AkkaAssertEqualityComparer
{
    public static readonly IEqualityComparer DefaultInnerComparer = new AkkaAssertEqualityComparerAdapter<object>(new AkkaAssertEqualityComparer<object?>());
    public static readonly Type NullableType = typeof(Nullable<>);
}

/// <summary>
/// Default implementation of <see cref="IEqualityComparer{T}"/> used by the Akka's xUnit.net equality assertions.
/// Copy of xUnits code
/// https://github.com/xunit/xunit/blob/3e6ab94ca231a6d8c86e90d6e724631a0faa33b7/src/xunit.assert/Asserts/Sdk/AssertEqualityComparer.cs
/// <remarks>Note! Part of internal API. Breaking changes may occur without notice. Use at own risk.</remarks>
/// </summary>
/// <typeparam name="T">The type that is being compared.</typeparam>
public class AkkaAssertEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<IEqualityComparer> _innerComparerFactory;
    private readonly bool _skipTypeCheck;

    /// <summary>
    /// Initializes a new instance of the <see cref="AkkaAssertEqualityComparer{T}" /> class.
    /// </summary>
    /// <param name="skipTypeCheck">Set to <c>true</c> to skip type equality checks.</param>
    /// <param name="innerComparer">The inner comparer to be used when the compared objects are enumerable.</param>
    public AkkaAssertEqualityComparer(bool skipTypeCheck = false, IEqualityComparer? innerComparer = null)
    {
        _skipTypeCheck = skipTypeCheck;

        // Use a thunk to delay evaluation of DefaultInnerComparer
        _innerComparerFactory = () => innerComparer ?? AkkaAssertEqualityComparer.DefaultInnerComparer;
    }

    public bool Equals(T? x, T? y)
    {
        var type = typeof(T);

        // Null?
        if(!type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableFrom(AkkaAssertEqualityComparer.NullableType)))
        {
            if(object.Equals(x, default(T)))
                return object.Equals(y, default(T));

            if(object.Equals(y, default(T)))
                return false;
        }

        switch (x)
        {
            case IEquatable<T> equatable:
                return equatable.Equals(y!);
            case IComparable<T> comparableGeneric:
                return comparableGeneric.CompareTo(y!) == 0;
            case IComparable comparable:
                return comparable.CompareTo(y) == 0;
            case IEnumerable enumerableX when y is IEnumerable enumerableY:
                var enumeratorX = enumerableX.GetEnumerator();
                var enumeratorY = enumerableY.GetEnumerator();
                var equalityComparer = _innerComparerFactory();

                try
                {
                    while (true)
                    {
                        var hasNextX = enumeratorX.MoveNext();
                        var hasNextY = enumeratorY.MoveNext();

                        if (!hasNextX || !hasNextY)
                            return (hasNextX == hasNextY);

                        if (!equalityComparer.Equals(enumeratorX.Current, enumeratorY.Current))
                            return false;
                    }
                }
                finally
                {
                    (enumeratorX as IDisposable)?.Dispose();
                    (enumeratorY as IDisposable)?.Dispose();
                }
        }

        // Same type?
        if (!_skipTypeCheck && x!.GetType() != y!.GetType())
            return false;

        // Last case, rely on Object.Equals
        return object.Equals(x, y);
    }

    public int GetHashCode(T obj) => throw new NotImplementedException();
}